﻿using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;
using RoslynHelper;

namespace NotifyPropertyChanged
{
    internal class NotifyPropertyChangedRewriter : SyntaxRewriter
    {
        private ISemanticModel _semanticModel;
        private List<FieldDeclarationSyntax> _newFields = new List<FieldDeclarationSyntax>();

        public NotifyPropertyChangedRewriter(ISemanticModel semanticModel)
        {
            this._semanticModel = semanticModel;
        }

        protected override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
        {
            if (!node.Attributes.SelectMany(a => a.Attributes).Any(a => Regex.IsMatch(a.Name.GetText(), "(^|.)NotifyPropertyChanged(Attribute)?$")))
                return node;
            return node.DefineAttributes(node.Attributes.Select(a => (AttributeDeclarationSyntax)Visit(a)))
                .AddInterface("System.ComponentModel.INotifyPropertyChanged")
                .DefineMembers(node.Members.Select(m => (MemberDeclarationSyntax)Visit(m))
                    .Union(_newFields)
                    .Union(new [] { Syntax.EventFieldDeclaration(
                        modifiers: Syntax.Token(SyntaxKind.PublicKeyword),
                        declaration: Syntax.VariableDeclaration(
                            type: Syntax.ParseTypeName("System.ComponentModel.PropertyChangedEventHandler"), 
                            variables: SeparatedList.CreateFromSingle(Syntax.VariableDeclarator(Syntax.Identifier("PropertyChanged"))))) }));
        }

        protected override SyntaxNode VisitAttributeDeclaration(AttributeDeclarationSyntax node)
        {
            var value = (AttributeDeclarationSyntax)base.VisitAttributeDeclaration(node);
            if (value.Attributes.All(a => a == null))
                return null;
            return value;
        }

        protected override SyntaxNode VisitAttribute(AttributeSyntax node)
        {
            return Regex.IsMatch(node.Name.GetText(), "(^|.)NotifyPropertyChanged(Attribute)?$") ? null : node;
        }

        protected override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node)
        {
            IEnumerable<AccessorDeclarationSyntax> accessors = node.AccessorList.Accessors;
            var getAccessor = accessors.FirstOrDefault(a => a.Kind == SyntaxKind.GetAccessorDeclaration);
            var setAccessor = accessors.FirstOrDefault(a => a.Kind == SyntaxKind.SetAccessorDeclaration);

            if (getAccessor == null || setAccessor == null)
                return node;

            var raisePropertyChanged = Syntax.IfStatement(
                condition: Syntax.BinaryExpression(
                    kind: SyntaxKind.NotEqualsExpression, 
                    left: (Syntax.IdentifierName("PropertyChanged")), 
                    right: LiteralExpression.Null()),
                statement: Syntax.ExpressionStatement(
                    Syntax.InvocationExpression(
                        expression:
                            Syntax.IdentifierName("PropertyChanged"),
                            argumentList: Syntax.ArgumentList(
                                arguments: SeparatedList.Create(
                                    Syntax.Argument(
                                        expression: Syntax.ThisExpression(
                                            Syntax.Token(SyntaxKind.ThisKeyword))),
                                    Syntax.Argument(
                                        expression:Syntax.ObjectCreationExpression(
                                            type: Syntax.IdentifierName("System.ComponentModel.PropertyChangedEventArgs"), 
                                            argumentListOpt: Syntax.ArgumentList(
                                                arguments: SeparatedList.CreateFromSingle(
                                                    Syntax.Argument(
                                                        expression: LiteralExpression.GetStringLiteral(node.Identifier.GetText())))))))))));

            if (setAccessor.ChildNodes().Any())
            {
                BlockSyntax blockSyntax = (BlockSyntax)setAccessor.ChildNodes().Single();
                setAccessor = setAccessor.DefineBody(blockSyntax.Statements.Union(new [] { raisePropertyChanged }));
            }
            else
            {
                string fieldName = node.Identifier.ToString();
                fieldName = Variable.GetVariableName(_semanticModel, string.Concat("_", fieldName.Substring(0, 1).ToLower(), fieldName.Substring(1).TrimEnd()));
                var field = Syntax.FieldDeclaration(
                    modifiers: Syntax.TokenList(
                        Syntax.Token(SyntaxKind.PrivateKeyword)),
                    declaration: Syntax.VariableDeclaration(
                        type: node.Type,
                        variables: SeparatedList.CreateFromSingle(Syntax.VariableDeclarator(
                            identifier: Syntax.Identifier(fieldName)))));
                _newFields.Add(field);

                getAccessor = getAccessor.DefineBody(Syntax.ReturnStatement(expressionOpt: Syntax.IdentifierName(fieldName)));

                setAccessor = setAccessor.DefineBody(new StatementSyntax[]
                                {
                                    Syntax.ExpressionStatement(
                                        Syntax.BinaryExpression(
                                            kind: SyntaxKind.AssignExpression, 
                                            left: Syntax.IdentifierName(fieldName),
                                            operatorToken: Syntax.Token(SyntaxKind.EqualsToken),
                                            right: Syntax.IdentifierName("value"))),
                                    raisePropertyChanged
                                });
            }
            return node.DefineAccessors(getAccessor, setAccessor);
        }
    }
}
